﻿/*******************************************************************************
 * Copyright (c) 2010-2011 Robert Fraser                                       *
 *                                                                             *
 * Permission is hereby granted, free of charge, to any person obtaining a     *
 * copy of this software and associated documentation files (the "Software"),  *
 * to deal in the Software without restriction, including without limitation   *
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,    *
 * and/or sell copies of the Software, and to permit persons to whom the       *
 * Software is furnished to do so, subject to the following conditions:        *
 *                                                                             *
 *   - The above copyright notice and this permission notice shall be included *
 *     in all copies or substantial portions of the Software.                  *
 *                                                                             *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  *
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,    *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER      *
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING     *
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER         *
 * DEALINGS IN THE SOFTWARE.                                                   *
 *******************************************************************************/

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using zeal.core;
using zeal.ui.util;
using zeal.util;

namespace zeal.ui.graph
{
    internal partial class GraphEditor
    {
        private readonly EditorMode _defaultMode;
        private EditorMode _mode;

        private void initEvents()
        {
            mouseEventSource.MouseDown += (_, args) =>
            {
                Point pos = getSurfacePosition(args);
                switch(args.ChangedButton) {
                    case MouseButton.Left:   _mode.onLeftMouseDown(pos);   break;
                    case MouseButton.Right:  _mode.onRightMouseDown(pos);  break;
                    case MouseButton.Middle: _mode.onMiddleMouseDown(pos); break; 
            }};

            mouseEventSource.MouseUp += (_, args) =>
            {
                Point pos = getSurfacePosition(args);
                switch(args.ChangedButton) {
                    case MouseButton.Left:   _mode.onLeftMouseUp(pos);   break;
                    case MouseButton.Right:  _mode.onRightMouseUp(pos);  break;
                    case MouseButton.Middle: _mode.onMiddleMouseUp(pos); break; 
            }};

            mouseEventSource.MouseMove += (_, args) =>  _mode.onMouseMove(getSurfacePosition(args));
            mouseEventSource.LostMouseCapture += (_, args) => _mode.onLostMouseCaptrure();
        }

        private bool isInDefaultMode { get { return _mode == _defaultMode; } }
        private void resetMode() { _mode = _defaultMode; }
        private Point getSurfacePosition(MouseEventArgs args) { return args.GetPosition(surface); }
        private bool hasMouseCapture { get { return Mouse.Captured == mouseEventSource; } }
        private static bool isMouseCaptured { get { return Mouse.Captured != null; } }
        private void beginCapture() { Debug.Assert(!isMouseCaptured); Mouse.Capture(mouseEventSource, CaptureMode.Element); }
        private void endCapture() { if(Mouse.Captured == mouseEventSource) Mouse.Capture(null); }
        private UIElement mouseEventSource { get { return (UIElement) _viewer.Content; } }

        private abstract class EditorMode
        {
            private readonly GraphEditor _editor;
            protected GraphEditor editor { get { return _editor; } }
            protected EditorMode(GraphEditor editor) { _editor = editor; }

            public virtual void onLeftMouseDown(Point pos) { }
            public virtual void onLeftMouseUp(Point pos) { }
            public virtual void onRightMouseDown(Point pos) { }
            public virtual void onRightMouseUp(Point pos) { }
            public virtual void onMiddleMouseDown(Point pos) { }
            public virtual void onMiddleMouseUp(Point pos) { }
            public virtual void onMouseMove(Point pos) { }
            public abstract void onLostMouseCaptrure();
        }

        private sealed class DefaultEditorMode : EditorMode
        {
            private readonly Rectangle _rect;
            private Point? _dragPos;
            private Point _dragStart;
            private MouseButton? _clickedButton;

            public DefaultEditorMode(GraphEditor editor) : base(editor)
            {
                _rect = new Rectangle
                {
                    StrokeThickness = 1.25,
                    Stroke = new SolidColorBrush(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)),
                    Fill = new SolidColorBrush(Color.FromArgb(0x33, 0xFF, 0xFF, 0xFF)),
                    StrokeDashArray = new DoubleCollection(new double[] { 5 }),
                    StrokeLineJoin = PenLineJoin.Bevel,
                    SnapsToDevicePixels = true,
                };
            }

            public override void onLeftMouseDown(Point pos) { if(begin(pos, MouseButton.Left, Cursors.Cross)) editor.overlay.addChild(_rect); editor.deselectAll(); }
            public override void onMiddleMouseDown(Point pos) { begin(pos, MouseButton.Middle, Cursors.ScrollAll); }
            private bool begin(Point pos, MouseButton button, Cursor cursor)
            {
                if(isMouseCaptured) return false;
                Debug.Assert(!_clickedButton.HasValue);
                editor.beginCapture();
                _dragPos = _dragStart = pos;
                _clickedButton = button;
                editor.cursor = cursor;
                return true;
            }

            public override void onLeftMouseUp(Point pos) { end(MouseButton.Left); }
            public override void onMiddleMouseUp(Point pos) { end(MouseButton.Middle); }
            private void end(MouseButton button)
            {
                if(!editor.hasMouseCapture || !_clickedButton.HasValue || _clickedButton.Value != button) return;
                Debug.Assert(_dragPos.HasValue);
                cleanup();
            }

            public override void onLostMouseCaptrure() { cleanup(); }
            private void cleanup()
            {
                editor.endCapture();
                editor.cursor = Cursors.Arrow;
                if(_clickedButton == MouseButton.Left)
                {
                    editor.overlay.removeChild(_rect);
                    _rect.Width = 0;
                    _rect.Height = 0;
                }
                _clickedButton = null;
                _dragPos = null;
            }
        
            public override void onMouseMove(Point pos)
            {
                if (!_dragPos.HasValue || !_clickedButton.HasValue) return;
                
                Vector v = pos - _dragStart;
                if (v.Length == 0) return;
                _dragPos = pos;

                MouseButton clicked = _clickedButton.Value;
                if(clicked == MouseButton.Left)        multiSelect(_dragStart, pos);
                else if(clicked == MouseButton.Middle) editor._viewer.pan(-v.X, -v.Y);
            }

            private void multiSelect(Point start, Point end)
            {
                // Update the adorner
                _rect.Width = Math.Abs(start.X - end.X);
                _rect.Height = Math.Abs(start.Y - end.Y);
                Canvas.SetLeft(_rect, Math.Min(start.X, end.X));
                Canvas.SetTop(_rect, Math.Min(start.Y, end.Y));

                // Update the set of selected nodes
                Rect area = new Rect(start, end);
                foreach(GraphNode node in editor.nodes)
                {
                    Rect bounds = new Rect(node.left, node.top, node.ActualWidth, node.ActualHeight);
                    node.IsSelected = bounds.IntersectsWith(area);
                }
            }

            // TODO -- this is just for testing
            public override void onRightMouseDown(Point pos) { if(!_clickedButton.HasValue) editor.pushUndo(editor.executeAdd(Tex2DNode.nodeInfo, pos)); }
        }

        private sealed class ConnectEditorMode : EditorMode, INotifyPropertyChanged
        {
            private static readonly ControlTemplate _tooltipTemplate = AssemblyResources.getApplicationResource<ControlTemplate>("ConnectErrorTooltipTemplate");
            private const double MIN_DRAG = GRID_SIZE;

            private readonly GraphPort _sourcePort;
            private readonly GraphPort _initialPort;
            private readonly BezierPath _edge;
            private readonly Control _tooltip;
            private readonly Point _start;
            private IMemento _memento;
            private GraphPort _over;
            private GraphEdge _deletionTarget;
            private bool _isDrag;
            private Point _mousePosition;
            private string _errorMessage;

            // Temporary for hit-testing; don't rely on these!
            private GraphNode _hitTestNode;
            private GraphPort _hitTestPort;

            // INotifyPropertyChanged stuff
            // ReSharper disable MemberCanBePrivate.Local
            // ReSharper disable UnusedMember.Local
            public event PropertyChangedEventHandler PropertyChanged;
            private static readonly PropertyChangedEventArgs _mousePositionChangedEventArgs = new PropertyChangedEventArgs("mousePosition");
            private static readonly PropertyChangedEventArgs _errorMessageChangedEventArgs = new PropertyChangedEventArgs("errorMessage");
            public Point mousePosition { get { return _mousePosition; } private set { if(value == _mousePosition) return; _mousePosition = value; PropertyChanged.raise(this, _mousePositionChangedEventArgs); } }
		    public string errorMessage { get { return _errorMessage; } private set { if(value == _errorMessage) return; _errorMessage = value; PropertyChanged.raise(this, _errorMessageChangedEventArgs); } }
            // ReSharper restore UnusedMember.Local
            // ReSharper restore MemberCanBePrivate.Local

            public ConnectEditorMode(GraphEditor editor, GraphPort clickedPort, Point mousePos) : base(editor)
            {
                if (editor == null) throw new ArgumentNullException("editor");
                if (clickedPort == null) throw new ArgumentNullException("clickedPort");
                Debug.Assert(Mouse.Captured == null);

                _start = mousePos;
                _initialPort = clickedPort;
                if (clickedPort.isInput)
                {
                    GraphEdge toDisconnect = clickedPort.connectedEdges.FirstOrDefault();
                    if (null != toDisconnect)
                    {
                        _sourcePort = toDisconnect.output;
                        _memento = editor.executeDisconnect(toDisconnect);
                    }
                }
                _sourcePort = _sourcePort ?? clickedPort;
                _sourcePort.ConnectionState = GraphPort.ConnectionStates.SOURCE;
                mousePosition = mousePos;
                editor.beginCapture();

                _edge = new BezierPath();
                _edge.Pen = EdgeColorizer.instance.getDefaultPen(_sourcePort.port.type);
                Bindings.set(_edge, _sourcePort.isInput ? BezierPath.DestinationProperty : BezierPath.SourceProperty, _sourcePort, GraphPort.AnchorPointProperty);
                Bindings.set(_edge, _sourcePort.isInput ? BezierPath.SourceProperty : BezierPath.DestinationProperty, this, "mousePosition");
                Panel.SetZIndex(_edge, Z_INDEX_TEMPORARY_EDGES);
                editor.surface.addChild(_edge);

                _tooltip = new Control { DataContext = this, Template = _tooltipTemplate, IsTabStop = false };
                Bindings.set(_tooltip, VisibilityProperty.of<Visibility>(), this, "errorMessage", (string msg) => string.IsNullOrEmpty(msg) ? Visibility.Collapsed : Visibility.Visible);
                editor._viewer.addTooltip(_tooltip, Bindings.get<Point>(this, "mousePosition"));

                _over = clickedPort;
                enterPort(clickedPort);
            }

            public override void onLeftMouseDown(Point pos) { checkAttach(pos); cleanup(); }
            public override void onLeftMouseUp(Point pos) { if(_isDrag) { checkAttach(pos); cleanup(); } }
            public override void onRightMouseDown(Point pos) { cleanup(); }
            public override void onLostMouseCaptrure() { cleanup(); }

            public override void onMouseMove(Point pos)
            {
                _isDrag |= Math.Abs(pos.X - _start.X) > MIN_DRAG;
                _isDrag |= Math.Abs(pos.Y - _start.Y) > MIN_DRAG;
                mousePosition = pos;
                GraphPort port = doHitTest(pos);
                if (port != _over)
                {
                    if (_over != null) leavePort(_over);
                    if (port != null) enterPort(port);
                    _over = port;
                }
            }

            private void checkAttach(Point pos)
            {
                Debug.Assert(editor.hasMouseCapture);
                GraphPort hitPort = doHitTest(pos);
                if (null != hitPort && hitPort.node != _sourcePort.node)
                {
                    if (hitPort.ConnectionState == GraphPort.ConnectionStates.VALID)
                    {
                        MultiMemento memento = new MultiMemento(""); // We'll give it the same name as the connect memmento
                        if (null != _memento)
                            memento.add(_memento);

                        bool wasSame = false;
                        if (null != _deletionTarget)
                        {
                            Debug.Assert(hitPort == _deletionTarget.input);
                            Debug.Assert(hitPort.isInput);

                            // If both the source and the destination are the same, don't bother doing anything
                            if(_sourcePort == _deletionTarget.output && hitPort == _deletionTarget.input)
                            {
                                _memento = null;
                                return;
                            }
                            memento.add(editor.executeDisconnect(_deletionTarget));
                            wasSame = hitPort == _initialPort;
                        }

                        IMemento connectMemmento = editor.executeConnect(_sourcePort, hitPort);
                        memento.add(connectMemmento);
                        memento.name = connectMemmento.name;

                        // If we hit the same port they started with, don't add this to the undo
                        _memento = wasSame ? null : memento;
                    }
                }
            }

            private void enterPort(GraphPort port)
            {
                if (port == null) throw new ArgumentNullException("port");
                if (port.node == _sourcePort.node)
                    return;
                Debug.Assert(port.ConnectionState == GraphPort.ConnectionStates.NONE);
                string error = null;

                if (_sourcePort.isInput == port.isInput)
                    error = "They are both " + (port.isInput ? "inputs" : "outputs");
                else if (!TextureType.canConvert(_sourcePort.port.type, port.port.type))
                    error = string.Format("{0}.{1} is of type {2}, but {3}.{4} is of type {5}",
                        _sourcePort.node.name, _sourcePort.name, _sourcePort.typeName,
                        port.node.name, port.name, port.typeName);
                else if (wouldIntroduceCycle(_sourcePort, port))
                    error = "They are mutually dependent";


                if (error != null)
                {
                    errorMessage = string.Format("Cannot connect {0}.{1} to {2}.{3}: {4}",
                        _sourcePort.node.name, _sourcePort.name, port.node.name, port.name, error);
                    port.ConnectionState = GraphPort.ConnectionStates.ERROR;
                }
                else
                {
                    port.ConnectionState = GraphPort.ConnectionStates.VALID;
                    // TODO -- mark the deletion targets
                    if (!_sourcePort.isInput)
                        _deletionTarget = port.connectedEdges.FirstOrDefault(e => e.input == port);
                }
            }

            private bool wouldIntroduceCycle(GraphPort port1, GraphPort port2)
            {
                Debug.Assert(port1.isInput != port2.isInput);
                TextureInputPort input = (TextureInputPort) (port1.isInput ? port1.port : port2.port);
                TextureOutputPort output = (TextureOutputPort) (port1.isInput ? port2.port : port1.port);
                return editor.graph.wouldIntroduceCycle(input, output);
            }

            private void leavePort(GraphPort port)
            {
                if (port == null) throw new ArgumentNullException("port");
                if (port.node == _sourcePort.node) return;
                Debug.Assert(port.ConnectionState != GraphPort.ConnectionStates.NONE);
                port.ConnectionState = GraphPort.ConnectionStates.NONE;
                errorMessage = null;
                _deletionTarget = null;
            }

            private void cleanup()
            {
                if (null != _over) leavePort(_over);
                _over = null;
                Debug.Assert(_sourcePort.ConnectionState == GraphPort.ConnectionStates.SOURCE);
                _sourcePort.ConnectionState = GraphPort.ConnectionStates.NONE;
                editor.surface.removeChild(_edge);
                editor._viewer.removeTooltip(_tooltip);
                if (null != _memento) editor.pushUndo(_memento);
                editor.resetMode();
                editor.endCapture();
            }

            private GraphPort doHitTest(Point pos)
            {
                // Since the surface has an applied RenderTransform, we must hit-test against the surface and not an ancestor
                Debug.Assert(_hitTestPort == null && _hitTestNode == null);
                VisualTreeHelper.HitTest(
                    editor.surface,
                    delegate(DependencyObject o)
                    {
                        _hitTestNode = _hitTestNode ?? o as GraphNode;
                        _hitTestPort = _hitTestPort ?? o as GraphPort;
                        return null != _hitTestPort && null != _hitTestNode ? HitTestFilterBehavior.Stop :
                            HitTestFilterBehavior.ContinueSkipSelf;
                    },
                    htr => HitTestResultBehavior.Continue,
                    new PointHitTestParameters(pos));

                GraphPort port = _hitTestPort ?? (_hitTestNode == null ? null :
                    (_sourcePort.isInput ? _hitTestNode.singleOutput : _hitTestNode.singleInput));
                _hitTestNode = null;
                _hitTestPort = null;
                return port;
            }
        }
    }
}